<?php

$file = __FILE__;
$pos = strripos($file, '/');
$includeDir = substr_replace($file, '', $pos+1);

require_once $includeDir.'/addendum/annotations.php';
require_once $includeDir.'/annotations/Resource.php';
require_once $includeDir.'/annotations/Component.php';
require_once $includeDir.'/exceptions/ComponentsException.php';
require_once $includeDir.'/exceptions/ClassNotFoundException.php';
require_once $includeDir.'/exceptions/FileNotFoundException.php';



class ComponentContext {
    private static $instances;
    private $configFile;
    private $componentsCache;
    private $obj = null;
    
    public function __construct($configFile = 'components.xml') {
        $this->configFile = $configFile;
    }
    
    public function getComponent($name) {
        $a = $this->executeXQuery("/default:components/default:annotation");
        if ($a) {
            $scanDir = (string)$a[0]['scan-dir'];

            $this->proccessDir($scanDir);
            
            if (array_key_exists($name, $this->componentsCache)) {
                $class = $this->componentsCache[$name];
                $object = $class->newInstance();
                $this->processAnnotationProperties($class, $object);
                return $object;
            }
        }

        $component = $this->getClassName($name);

        $className = (string)$component['class'];
        $baseDir = (string)$component['base-dir'];
        
        if (self::$instances[$name]) {
            return self::$instances[$name];
        }
        
        $className = $this->getClass($className, $baseDir);
        $class = new ReflectionAnnotatedClass($className);

        $object = $this->getInstance($component, $class);
        
        if ($component['scope'] == 'singleton') {
            self::$instances[$name] = $object;
        }
        
        $this->processAnnotationProperties($class, $object);
        $this->processXmlProperties($component, $class, $object);
        
        return $object;
    }

    private function proccessDir($scanDir) {
        $handle = opendir($scanDir);
        while (false !== ($file = readdir($handle))) {
            if (is_dir($scanDir.'/'.$file)) {
                if ($file != '.' && $file != '..') {
                    $scanDir .= '/'.$file;
                    $this->proccessDir($scanDir);
                }
            } else {
                $php = strpos($file, '.php');
                if ($php !== false) {
                    $className = substr($file, 0, $php);
                    if (class_exists($className)) {
                        $this->populateComponentsCache($className);
                    } else {
                        require_once $scanDir.'/'.$file;
                        if (class_exists($className)) {
                            $this->populateComponentsCache($className);
                        }
                    }
                }
            }
        }
    }

    private function populateComponentsCache($className) {
        $class = new ReflectionAnnotatedClass($className);
        if ($class->hasAnnotation('Component')) {
            $bindName = $class->getAnnotation('Component')->value;
            $this->componentsCache[$bindName] = $class;
        }
    }

    private function getClassName($componentName) {
        $xquery = "/default:components/default:component[@id='$componentName']";
        $c = $this->executeXQuery($xquery);

        return $c[0];
    }

    private function getEnviroment() {
        return $this->executeXQuery("/default:components/default:enviroment");
    }

    private function getBaseDir() {
        $env = $this->getEnviroment();
        
        return (string)($env[0]->{'base-dir'})?$env[0]->{'base-dir'}:'.';
    }

    private function getXmlObject() {
        if ($this->obj === null) {
            $xml = file_get_contents($this->configFile);

            $this->obj = new SimpleXMLElement($xml);
            $namespaces = $this->obj->getNamespaces(true);
            $this->obj->registerXPathNamespace("default", $namespaces[""]);
        }
        return $this->obj;
    }

    private function executeXQuery($xquery) {
        $xmlObject = $this->getXmlObject();
        $rs = $xmlObject->xpath($xquery);
        return $rs;
    }

    private function getClass($className, $baseDir = null) {
        if (!class_exists($className)) {
            $globalBaseDir = $this->getBaseDir();

            $pos = strripos($className, '.');
            $file = str_replace('.', '/', $className);
            $file = ($baseDir)?$baseDir.'/'.$file:$globalBaseDir.'/'.$file;

            if (!file_exists($file.'.php')) {
                if (file_exists($file.'.class.php')) {
                    $file = $file.'.class';
                } else {
                    throw new FileNotFoundException("Arquivo $file inexistente");
                }
            }
            $file .= '.php';
            $className = ($pos === false)?$className:substr($className, $pos+1);

            require_once $file;
            if (!class_exists($className)) {
                throw new ClassNotFoundException("Classe $className inexistente");
            }
        }
        return $className;
    }

    private function getValue($obj) {
        $value = null;
        if ($obj->value) {
            $value = $obj->value;
        } else {
            $value = $this->getComponent($obj->ref['id']);
        }
        return $value;
    }

    private function processXmlProperties($properties, $class, $object) {
        if ($properties->property) {
            foreach ($properties->property as $p) {
                if ($class->hasProperty($p['name'])) {
                    $methodName = "set".ucfirst($p['name']);
                    $value = $this->getValue($p);
                    $set = $class->getMethod($methodName);
                    $set->invoke($object, $value);
                }
            }
        }
    }

    private function processAnnotationProperties($class, $object) {
        $properties = $class->getProperties();
        foreach ($properties as $p) {
            if ($p->hasAnnotation('Resource')) {
                $propertyName = $p->getName();
                $methodName = "set".ucfirst($propertyName);

                $objectName = ($p->getAnnotation('Resource')->value)?$p->getAnnotation('Resource')->value:$p->getName();

                $obj = $this->getComponent($objectName);

                $set = $class->getMethod($methodName);
                $set->invoke($object, $obj);
            }
        }
    }

    private function getInstance($constructor, $class) {
        if ($constructor->{'constructor-arg'}) {
            $args = array();
            foreach ($constructor->{'constructor-arg'} as $arg) {
                $args[] = $this->getValue($arg);
            }
            $object = $class->newInstanceArgs($args);
        } else {
            $object = $class->newInstance();
        }

        return $object;
    }
}
